Python
Python 自定义异常:设计清晰的错误体系
说明何时需要自定义异常,以及异常继承、错误上下文和业务异常体系的设计方法。
为什么需要自定义异常
内置异常(ValueError、IOError 等)是通用的,无法满足业务场景的三类需求:
| 问题 | 说明 |
|---|---|
| 语义不清晰 | ValueError 无法区分"年龄是负数"还是"邮箱格式错了" |
| 无法附加上下文 | 需要携带错误码、用户 ID、请求 URL 等业务数据 |
| 无法精确捕获 | except ValueError 会捕获所有 ValueError,包括不想处理的 |
继承体系

IMPORTANT 继承
Exception而非BaseExceptionSystemExit、KeyboardInterrupt继承自BaseException,不该被普通业务代码捕获。自定义异常始终继承Exception。
定义方式
最简定义
class InsufficientFundsError(Exception):
pass两行即可,已是完整的自定义异常。
携带自定义属性
class InsufficientFundsError(Exception):
def __init__(self, amount, balance):
self.amount = amount
self.balance = balance
self.shortfall = amount - balance
super().__init__(
f"尝试取款 {amount} 元,余额仅 {balance} 元,差额 {self.shortfall} 元"
)
def __str__(self):
return '这是print打印异常输出的信息'TIP 务必调用
super().__init__(message)这决定了str(e)和repr(e)的输出内容,不调用则打印异常时没有信息。
抛出与捕获
基本用法
def withdraw(amount, balance):
if amount > balance:
raise InsufficientFundsError(amount, balance)
try:
withdraw(500, 100)
except InsufficientFundsError as e:
print(e.shortfall) # 400
print(str(e)) # 尝试取款 500 元,余额仅 100 元,差额 400 元except 的捕获机制
NOTE 核心原理
except底层使用isinstance()判断,捕获的是类型身份(继承关系),与属性无关。
isinstance(InsufficientFundsError(), InsufficientFundsError) # True
isinstance(InsufficientFundsError(), BankError) # True ← 父类也能捕获
isinstance(InsufficientFundsError(), ValueError) # False ← 无继承关系,捕获不到方向规则:只能子类→父类,不能反向。
# ✅ raise 子类 → except 父类:可以捕获
try:
raise InsufficientFundsError(500, 100)
except BankError as e:
print(type(e)) # <class 'InsufficientFundsError'>
print(e.shortfall) # 400 ← 子类属性仍然可以访问
# ❌ raise 父类 → except 子类:捕获不到,异常向外抛出
try:
raise Exception("父类异常")
except InsufficientFundsError:
print("不会执行")
# ❌ raise 无关类 → except 自定义类:捕获不到
try:
raise ValueError("无关异常")
except InsufficientFundsError:
print("不会执行")WARNING 没有
raise或类型对不上,except直接跳过except是守门人,只认"血统"(继承链),不管try里写了什么,只看最终raise出来的类型。
分级捕获(工程实践)
建立模块异常体系
# 模块基类
class BankError(Exception):
"""银行业务异常基类"""
pass
# 具体子类
class InsufficientFundsError(BankError):
def __init__(self, amount, balance):
self.shortfall = amount - balance
super().__init__(f"余额不足,差额 {self.shortfall}")
class AccountFrozenError(BankError):
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"账户 {account_id} 已被冻结")
class DailyLimitExceededError(BankError):
pass分级捕获
try:
transfer(...)
except InsufficientFundsError as e: # 最具体,写最前面
notify_user(e.shortfall)
except BankError as e: # 兜底:所有银行业务异常
log_error(e)
except Exception as e: # 最后防线:未预期异常
alert_oncall(e)IMPORTANT
except从上到下顺序匹配,最具体的写最前面。
异常链
表达"因为 X 所以发生了 Y":
def load_config(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError as e:
raise ConfigError(f"配置文件不存在: {path}") from e
# from None ← 不想暴露底层异常时使用from e 将原始异常挂在 __cause__ 上,traceback 显示完整因果链,调试时非常有用。
工程实践建议
- 一个模块一个基类:
XxxError(Exception),具体异常继承它 - 异常类放在单独文件:通常命名
exceptions.py,避免循环导入 - 写 docstring:说明"什么情况下抛出"
- 不要滥用:内置异常语义已够精确时(如
TypeError)无需自定义,自定义异常的价值在于业务语义
速查总结
| 场景 | 结果 |
|---|---|
raise 子类 → except 父类 |
✅ 捕获成功 |
raise 父类 → except 子类 |
❌ 捕获失败,异常向外抛 |
raise 无关类 → except 自定义 |
❌ 捕获失败,异常向外抛 |
| 捕获后访问子类属性 | ✅ 可以,e 仍是子类完整实例 |